package edu.fourtwelve.util;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 *
 * @author Christopher C. Hunt
 */
public class SymetricKeyGen implements BankingConstants {

    private static final Logger log = Logger.getLogger(SymetricKeyGen.class.getName());
    private SecretKey secretKey;
    private IvParameterSpec ivp;
    private final int SYM_KEY_SIZE = 256;

    public SymetricKeyGen() throws BankingException {
        try {
            byte[] salt = new byte[20];
            byte[] iv = new byte[16];
            char[] password = new char[20];
            final int KEY_SIZE = 20;
            SecureRandom sr;
            try {
                sr = SecureRandom.getInstance("SHA1PRNG");

                byte[] seed = sr.generateSeed(KEY_SIZE * 4);
                
                int[] msSeed = new int[KEY_SIZE];
                for (int i = 0; i < KEY_SIZE * 4; i += 4) {
                    msSeed[i % 4] = Utils.byteArrayToInt(new byte[]{seed[i], seed[i + 1], seed[i + 2], seed[i + 3]});
                }
                MersenneTwister msRand = new MersenneTwister(msSeed);
                
                msRand.nextBytes(seed);
                sr.setSeed(seed);
                sr.nextBytes(salt);
                
                msRand.nextBytes(seed);
                sr.setSeed(seed);
                byte[] pass = new byte[20];
                for (int i = 0; i < pass.length; i++) {
                    pass[i] = (byte) ((char) (sr.nextInt(94) + 33));
                }

//                sr.nextBytes(pass);
//                for(int i=0; i<password.length; i+=2) {
//                    password[i] = (char)(((pass[i]&0x00FF)<<8) + (pass[i+1]&0x00FF));                   
//                }

                for (int i = 0; i < pass.length; i++) {
                    password[i] = (char) pass[i];
                }
                
                msRand.nextBytes(seed);
                sr.setSeed(seed);
                sr.nextBytes(iv);

                log.log(Level.INFO, "salt= {0} pass= {1} password= {2}, iv= {3}",
                        new Object[]{Utils.toHexString(salt), Utils.toHexString(pass),
                            new String(password), Utils.toHexString(iv)});
            } catch (NoSuchAlgorithmException ex) {
                log.log(Level.SEVERE, null, ex);
                throw new BankingException("Random Generation Failure");
            }

            SecretKeyFactory factory = SecretKeyFactory.getInstance(SYMETRIC_KEY_FACTORY);
            KeySpec spec = new PBEKeySpec(password, salt, 65536, SYM_KEY_SIZE);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), BASE_SYMETRIC_ALGORITM);

            secretKey = secret;
            ivp = new IvParameterSpec(iv);
        } catch (NoSuchAlgorithmException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException("Invalid Algorithm");
        } catch (InvalidKeySpecException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException("Invalid Key Spec");
        }
    }

    public SymetricKeyGen(char[] password, byte[] iv) throws BankingException {
        final int KEY_SIZE = 20;
        try {
            byte[] salt = new byte[20];

            SecureRandom sr;
            try {
                sr = SecureRandom.getInstance("SHA1PRNG");
                byte[] seed = sr.generateSeed(KEY_SIZE * 4);
                
                int[] msSeed = new int[KEY_SIZE];
                for (int i = 0; i < KEY_SIZE * 4; i += 4) {
                    msSeed[i % 4] = Utils.byteArrayToInt(new byte[]{seed[i], seed[i + 1], seed[i + 2], seed[i + 3]});
                }
                MersenneTwister msRand = new MersenneTwister(msSeed);
                
                msRand.nextBytes(seed);
                sr.setSeed(seed);
                sr.nextBytes(salt);

                log.log(Level.INFO, "salt= {0} password= {1}, iv= {2}",
                        new Object[]{Utils.toHexString(salt),
                            new String(password), Utils.toHexString(iv)});
            } catch (NoSuchAlgorithmException ex) {
                log.log(Level.SEVERE, null, ex);
                throw new BankingException("Random Generation Failure");
            }

            SecretKeyFactory factory = SecretKeyFactory.getInstance(SYMETRIC_KEY_FACTORY);
            KeySpec spec = new PBEKeySpec(password, salt, 65536, SYM_KEY_SIZE);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), BASE_SYMETRIC_ALGORITM);

            secretKey = secret;
            ivp = new IvParameterSpec(iv);
        } catch (NoSuchAlgorithmException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException("Invalid Algorithm");
        } catch (InvalidKeySpecException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException("Invalid Key Spec");
        }
    }

    public SymetricKeyGen(char[] password, byte[] salt, byte[] iv) throws BankingException {
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance(SYMETRIC_KEY_FACTORY);

            KeySpec spec = new PBEKeySpec(password, salt, 65536, SYM_KEY_SIZE);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), BASE_SYMETRIC_ALGORITM);

            secretKey = secret;
            ivp = new IvParameterSpec(iv);
        } catch (NoSuchAlgorithmException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException("Invalid Algorithm");
        } catch (InvalidKeySpecException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException("Invalid Key Spec");
        }
    }

    public SymetricKeyGen(SecretKey secret, IvParameterSpec ivp) throws BankingException {
        secretKey = secret;
        this.ivp = ivp;
    }

    public SymetricKeyGen(SecretKey secret, byte[] iv) throws BankingException {
        secretKey = secret;
        this.ivp = new IvParameterSpec(iv);
    }

    public byte[] encrypt(byte[] plaintext) throws BankingException {
        byte[] retVal = null;
        try {
            Cipher cipher = Cipher.getInstance(SYMETRIC_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivp);

            retVal = cipher.doFinal(plaintext);
        } catch (NoSuchAlgorithmException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (NoSuchPaddingException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (InvalidKeyException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (InvalidAlgorithmParameterException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (IllegalBlockSizeException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (BadPaddingException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        }

        return retVal;
    }

    public byte[] decrypt(byte[] ciphertext) throws BankingException {
        byte[] retVal = null;
        try {
            Cipher cipher = Cipher.getInstance(SYMETRIC_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivp);

            retVal = cipher.doFinal(ciphertext);
        } catch (NoSuchAlgorithmException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (NoSuchPaddingException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (InvalidKeyException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (InvalidAlgorithmParameterException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (IllegalBlockSizeException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        } catch (BadPaddingException ex) {
            log.log(Level.SEVERE, null, ex);
            throw new BankingException(ex.getMessage());
        }

        return retVal;
    }

    public synchronized SecretKey getSecretKey() {
        return secretKey;
    }

    public synchronized IvParameterSpec getIVParams() {
        return ivp;
    }

    public synchronized byte[] getIV() {
        return ivp.getIV();
    }
}
